Français

Explorez des patrons avancés pour l'API Context de React, incluant les composants composés, les contextes dynamiques, et des techniques d'optimisation pour la gestion d'états complexes.

Patrons Avancés de l'API Context de React pour la Gestion d'État

L'API Context de React fournit un mécanisme puissant pour partager l'état à travers votre application sans avoir à passer des props de haut en bas ("prop drilling"). Bien que son utilisation de base soit simple, exploiter tout son potentiel nécessite de comprendre des patrons avancés capables de gérer des scénarios complexes de gestion d'état. Cet article explore plusieurs de ces patrons, offrant des exemples pratiques et des perspectives concrètes pour améliorer votre développement React.

Comprendre les Limites de l'API Context de Base

Avant de plonger dans les patrons avancés, il est crucial de reconnaître les limites de l'API Context de base. Bien qu'adaptée à un état simple et globalement accessible, elle peut devenir difficile à gérer et inefficace pour les applications complexes avec un état qui change fréquemment. Chaque composant consommant un contexte se re-rend chaque fois que la valeur du contexte change, même si le composant ne dépend pas de la partie spécifique de l'état qui a été mise à jour. Cela peut entraîner des goulots d'étranglement en termes de performance.

Patron 1 : Composants Composés avec le Contexte

Le patron des Composants Composés (Compound Components) améliore l'API Context en créant une suite de composants liés qui partagent implicitement l'état et la logique via un contexte. Ce patron favorise la réutilisabilité et simplifie l'API pour les consommateurs. Cela permet d'encapsuler une logique complexe avec une implémentation simple.

Exemple : Un Composant d'Onglets

Illustrons cela avec un composant d'onglets. Au lieu de passer des props à travers plusieurs niveaux, les composants Tab communiquent implicitement via un contexte partagé.

// TabContext.js
import React, { createContext, useContext, useState, ReactNode } from 'react';

interface TabContextType {
  activeTab: string;
  setActiveTab: (tab: string) => void;
}

const TabContext = createContext(undefined);

interface TabProviderProps {
  children: ReactNode;
  defaultTab: string;
}

export const TabProvider: React.FC = ({ children, defaultTab }) => {
  const [activeTab, setActiveTab] = useState(defaultTab);

  const value: TabContextType = {
    activeTab,
    setActiveTab,
  };

  return {children};
};

export const useTabContext = () => {
  const context = useContext(TabContext);
  if (!context) {
    throw new Error('useTabContext doit être utilisé à l\'intérieur d\'un TabProvider');
  }
  return context;
};

// TabList.js
import React, { ReactNode } from 'react';

interface TabListProps {
  children: ReactNode;
}

export const TabList: React.FC = ({ children }) => {
  return 
{children}
; }; // Tab.js import React, { ReactNode } from 'react'; import { useTabContext } from './TabContext'; interface TabProps { label: string; children: ReactNode; } export const Tab: React.FC = ({ label, children }) => { const { activeTab, setActiveTab } = useTabContext(); const isActive = activeTab === label; const handleClick = () => { setActiveTab(label); }; return ( ); }; // TabPanel.js import React, { ReactNode } from 'react'; import { useTabContext } from './TabContext'; interface TabPanelProps { label: string; children: ReactNode; } export const TabPanel: React.FC = ({ label, children }) => { const { activeTab } = useTabContext(); const isActive = activeTab === label; return ( ); };
// Utilisation
import { TabProvider, TabList, Tab, TabPanel } from './components/Tabs';

function App() {
  return (
    
      
        Onglet 1
        Onglet 2
        Onglet 3
      
      Contenu de l'onglet 1
      Contenu de l'onglet 2
      Contenu de l'onglet 3
    
  );
}

export default App;

Avantages :

Patron 2 : Contextes Dynamiques

Dans certains scénarios, vous pourriez avoir besoin de différentes valeurs de contexte en fonction de la position du composant dans l'arborescence des composants ou d'autres facteurs dynamiques. Les contextes dynamiques vous permettent de créer et de fournir des valeurs de contexte qui varient en fonction de conditions spécifiques.

Exemple : Création de thèmes avec des contextes dynamiques

Considérez un système de thèmes où vous souhaitez fournir différents thèmes en fonction des préférences de l'utilisateur ou de la section de l'application dans laquelle il se trouve. Nous pouvons créer un exemple simplifié avec un thème clair et un thème sombre.

// ThemeContext.js
import React, { createContext, useContext, useState, ReactNode } from 'react';

interface Theme {
  background: string;
  color: string;
}

interface ThemeContextType {
  theme: Theme;
  toggleTheme: () => void;
}

const defaultTheme: Theme = {
    background: 'white',
    color: 'black'
};

const darkTheme: Theme = {
    background: 'black',
    color: 'white'
};

const ThemeContext = createContext({
    theme: defaultTheme,
    toggleTheme: () => {}
});

interface ThemeProviderProps {
  children: ReactNode;
}

export const ThemeProvider: React.FC = ({ children }) => {
  const [isDarkTheme, setIsDarkTheme] = useState(false);
  const theme = isDarkTheme ? darkTheme : defaultTheme;

  const toggleTheme = () => {
    setIsDarkTheme(!isDarkTheme);
  };

  const value: ThemeContextType = {
    theme,
    toggleTheme,
  };

  return {children};
};

export const useTheme = () => {
  return useContext(ThemeContext);
};
// Utilisation
import { useTheme, ThemeProvider } from './ThemeContext';

function MyComponent() {
  const { theme, toggleTheme } = useTheme();

  return (
    

Ceci est un composant à thème.

); } function App() { return ( ); } export default App;

Dans cet exemple, le ThemeProvider détermine dynamiquement le thème en fonction de l'état isDarkTheme. Les composants utilisant le hook useTheme se re-rendront automatiquement lorsque le thème changera.

Patron 3 : Contexte avec useReducer pour un État Complexe

Pour gérer une logique d'état complexe, combiner l'API Context avec useReducer est une excellente approche. useReducer fournit une manière structurée de mettre à jour l'état en fonction d'actions, et l'API Context vous permet de partager cet état et la fonction de dispatch à travers votre application.

Exemple : Une Simple Liste de Tâches

// TodoContext.js
import React, { createContext, useContext, useReducer, ReactNode } from 'react';

interface Todo {
  id: number;
  text: string;
  completed: boolean;
}

interface TodoState {
  todos: Todo[];
}

type TodoAction = 
  | { type: 'ADD_TODO'; text: string } 
  | { type: 'TOGGLE_TODO'; id: number } 
  | { type: 'DELETE_TODO'; id: number };

interface TodoContextType {
  state: TodoState;
  dispatch: React.Dispatch;
}

const initialState: TodoState = {
  todos: [],
};

const todoReducer = (state: TodoState, action: TodoAction): TodoState => {
  switch (action.type) {
    case 'ADD_TODO':
      return {
        ...state,
        todos: [...state.todos, { id: Date.now(), text: action.text, completed: false }],
      };
    case 'TOGGLE_TODO':
      return {
        ...state,
        todos: state.todos.map((todo) =>
          todo.id === action.id ? { ...todo, completed: !todo.completed } : todo
        ),
      };
    case 'DELETE_TODO':
      return {
        ...state,
        todos: state.todos.filter((todo) => todo.id !== action.id),
      };
    default:
      return state;
  }
};

const TodoContext = createContext(undefined);

interface TodoProviderProps {
  children: ReactNode;
}

export const TodoProvider: React.FC = ({ children }) => {
  const [state, dispatch] = useReducer(todoReducer, initialState);

  const value: TodoContextType = {
    state,
    dispatch,
  };

  return {children};
};

export const useTodo = () => {
  const context = useContext(TodoContext);
  if (!context) {
    throw new Error('useTodo doit être utilisé à l\'intérieur d\'un TodoProvider');
  }
  return context;
};
// Utilisation
import { useTodo, TodoProvider } from './TodoContext';

function TodoList() {
  const { state, dispatch } = useTodo();

  return (
    
    {state.todos.map((todo) => (
  • {todo.text}
  • ))}
); } function AddTodo() { const { dispatch } = useTodo(); const [text, setText] = React.useState(''); const handleSubmit = (e) => { e.preventDefault(); dispatch({ type: 'ADD_TODO', text }); setText(''); }; return (
setText(e.target.value)} />
); } function App() { return ( ); } export default App;

Ce patron centralise la logique de gestion de l'état au sein du reducer, ce qui la rend plus facile à comprendre et à tester. Les composants peuvent dispatcher des actions pour mettre à jour l'état sans avoir à gérer l'état directement.

Patron 4 : Mises à Jour de Contexte Optimisées avec `useMemo` et `useCallback`

Comme mentionné précédemment, une considération de performance clé avec l'API Context est les re-renders inutiles. L'utilisation de useMemo et useCallback peut empêcher ces re-renders en s'assurant que seules les parties nécessaires de la valeur du contexte sont mises à jour, et que les références de fonction restent stables.

Exemple : Optimisation d'un Contexte de Thème

// OptimizedThemeContext.js
import React, { createContext, useContext, useState, useMemo, useCallback, ReactNode } from 'react';

interface Theme {
  background: string;
  color: string;
}

interface ThemeContextType {
  theme: Theme;
  toggleTheme: () => void;
}

const defaultTheme: Theme = {
    background: 'white',
    color: 'black'
};

const darkTheme: Theme = {
    background: 'black',
    color: 'white'
};

const ThemeContext = createContext({
    theme: defaultTheme,
    toggleTheme: () => {}
});

interface ThemeProviderProps {
  children: ReactNode;
}

export const ThemeProvider: React.FC = ({ children }) => {
  const [isDarkTheme, setIsDarkTheme] = useState(false);
  const theme = isDarkTheme ? darkTheme : defaultTheme;

  const toggleTheme = useCallback(() => {
    setIsDarkTheme(!isDarkTheme);
  }, [isDarkTheme]);

  const value: ThemeContextType = useMemo(() => ({
    theme,
    toggleTheme,
  }), [theme, toggleTheme]);

  return {children};
};

export const useTheme = () => {
  return useContext(ThemeContext);
};

Explication :

Sans useCallback, la fonction toggleTheme serait recréée à chaque rendu du ThemeProvider, ce qui ferait changer la value et déclencherait des re-renders dans tous les composants consommateurs, même si le thème lui-même n'avait pas changé. useMemo garantit qu'une nouvelle value n'est créée que lorsque ses dépendances (theme ou toggleTheme) changent.

Patron 5 : Sélecteurs de Contexte

Les sélecteurs de contexte permettent aux composants de s'abonner uniquement à des parties spécifiques de la valeur du contexte. Cela évite les re-renders inutiles lorsque d'autres parties du contexte changent. Des bibliothèques comme `use-context-selector` ou des implémentations personnalisées peuvent être utilisées pour y parvenir.

Exemple d'Utilisation d'un Sélecteur de Contexte Personnalisé

// useCustomContextSelector.js
import { useContext, useState, useRef, useEffect } from 'react';

function useCustomContextSelector(
  context: React.Context,
  selector: (value: T) => S
): S {
  const value = useContext(context);
  const [selected, setSelected] = useState(() => selector(value));
  const latestSelector = useRef(selector);
  latestSelector.current = selector;

  useEffect(() => {
    let didUnmount = false;
    let lastSelected = selected;

    const subscription = () => {
      if (didUnmount) {
        return;
      }
      const nextSelected = latestSelector.current(value);
      if (!Object.is(lastSelected, nextSelected)) {
        lastSelected = nextSelected;
        setSelected(nextSelected);
      }
    };

    // Normalement, vous vous abonneriez aux changements de contexte ici. Comme c'est un exemple simplifié,
    // nous appellerons simplement la souscription immédiatement pour initialiser.
    subscription();

    return () => {
      didUnmount = true;
      // Se désabonner des changements de contexte ici, si applicable.
    };
  }, [value]); // Réexécute l'effet chaque fois que la valeur du contexte change

  return selected;
}

export default useCustomContextSelector;
// ThemeContext.js (Simplifié pour la brièveté)
import React, { createContext, useState, ReactNode } from 'react';

interface Theme {
  background: string;
  color: string;
}

interface ThemeContextType {
  theme: Theme;
  setTheme: (newTheme: Theme) => void; 
}

const ThemeContext = createContext(undefined);

interface ThemeProviderProps {
  children: ReactNode;
  initialTheme: Theme;
}

export const ThemeProvider: React.FC = ({ children, initialTheme }) => {
  const [theme, setTheme] = useState(initialTheme);

  const value: ThemeContextType = {
    theme,
    setTheme
  };

  return {children};
};

export const useThemeContext = () => {
    const context = React.useContext(ThemeContext);
    if (!context) {
        throw new Error("useThemeContext doit être utilisé à l'intérieur d'un ThemeProvider");
    }
    return context;
};

export default ThemeContext;
// Utilisation
import useCustomContextSelector from './useCustomContextSelector';
import ThemeContext, { ThemeProvider, useThemeContext } from './ThemeContext';

function BackgroundComponent() {
  const background = useCustomContextSelector(ThemeContext, (context) => context.theme.background);
  return 
Arrière-plan
; } function ColorComponent() { const color = useCustomContextSelector(ThemeContext, (context) => context.theme.color); return
Couleur
; } function App() { const { theme, setTheme } = useThemeContext(); const toggleTheme = () => { setTheme({ background: theme.background === 'white' ? 'black' : 'white', color: theme.color === 'black' ? 'white' : 'black' }); }; return ( ); } export default App;

Dans cet exemple, BackgroundComponent ne se re-rend que lorsque la propriété background du thème change, et ColorComponent ne se re-rend que lorsque la propriété color change. Cela évite les re-renders inutiles lorsque la valeur entière du contexte change.

Patron 6 : Séparer les Actions de l'État

Pour les applications plus volumineuses, envisagez de séparer la valeur du contexte en deux contextes distincts : un pour l'état et un autre pour les actions (fonctions de dispatch). Cela peut améliorer l'organisation du code et la testabilité.

Exemple : Liste de Tâches avec des Contextes d'État et d'Action Séparés

// TodoStateContext.js
import React, { createContext, useContext, useReducer, ReactNode } from 'react';

interface Todo {
  id: number;
  text: string;
  completed: boolean;
}

interface TodoState {
  todos: Todo[];
}

const initialState: TodoState = {
  todos: [],
};

const TodoStateContext = createContext(initialState);

interface TodoStateProviderProps {
  children: ReactNode;
}

export const TodoStateProvider: React.FC = ({ children }) => {
  const [state] = useReducer(todoReducer, initialState);

  return {children};
};

export const useTodoState = () => {
  return useContext(TodoStateContext);
};

// TodoActionContext.js
import React, { createContext, useContext, Dispatch, ReactNode } from 'react';

type TodoAction = 
  | { type: 'ADD_TODO'; text: string } 
  | { type: 'TOGGLE_TODO'; id: number } 
  | { type: 'DELETE_TODO'; id: number };

const TodoActionContext = createContext | undefined>(undefined);

interface TodoActionProviderProps {
    children: ReactNode;
}

export const TodoActionProvider: React.FC = ({children}) => {
    const [, dispatch] = useReducer(todoReducer, initialState);

    return {children};
};


export const useTodoDispatch = () => {
  const dispatch = useContext(TodoActionContext);
  if (!dispatch) {
    throw new Error('useTodoDispatch doit être utilisé à l\'intérieur d\'un TodoActionProvider');
  }
  return dispatch;
};

// todoReducer.js
export const todoReducer = (state: TodoState, action: TodoAction): TodoState => {
  switch (action.type) {
    case 'ADD_TODO':
      return {
        ...state,
        todos: [...state.todos, { id: Date.now(), text: action.text, completed: false }],
      };
    case 'TOGGLE_TODO':
      return {
        ...state,
        todos: state.todos.map((todo) =>
          todo.id === action.id ? { ...todo, completed: !todo.completed } : todo
        ),
      };
    case 'DELETE_TODO':
      return {
        ...state,
        todos: state.todos.filter((todo) => todo.id !== action.id),
      };
    default:
      return state;
  }
};
// Utilisation
import { useTodoState, TodoStateProvider } from './TodoStateContext';
import { useTodoDispatch, TodoActionProvider } from './TodoActionContext';

function TodoList() {
  const state = useTodoState();

  return (
    
    {state.todos.map((todo) => (
  • {todo.text}
  • ))}
); } function TodoActions({ todo }) { const dispatch = useTodoDispatch(); return ( <> ); } function AddTodo() { const dispatch = useTodoDispatch(); const [text, setText] = React.useState(''); const handleSubmit = (e) => { e.preventDefault(); dispatch({ type: 'ADD_TODO', text }); setText(''); }; return (
setText(e.target.value)} />
); } function App() { return ( ); } export default App;

Cette séparation permet aux composants de ne s'abonner qu'au contexte dont ils ont besoin, réduisant ainsi les re-renders inutiles. Elle facilite également les tests unitaires du reducer et de chaque composant de manière isolée. De plus, l'ordre d'imbrication des fournisseurs est important. Le ActionProvider doit englober le StateProvider.

Meilleures Pratiques et Considérations

Conclusion

L'API Context de React est un outil polyvalent pour la gestion d'état. En comprenant et en appliquant ces patrons avancés, vous pouvez gérer efficacement un état complexe, optimiser les performances et créer des applications React plus maintenables et évolutives. N'oubliez pas de choisir le bon patron pour vos besoins spécifiques et de considérer attentivement les implications de performance de votre utilisation du contexte.

À mesure que React évolue, les meilleures pratiques entourant l'API Context évolueront également. Rester informé des nouvelles techniques et bibliothèques vous assurera d'être équipé pour relever les défis de la gestion d'état dans le développement web moderne. Envisagez d'explorer les patrons émergents comme l'utilisation du contexte avec des signaux pour une réactivité encore plus fine.